Tagless Final
#scala
言語内DSL(EDSL)をScalaで構築する手法
Tagless Final Styleと呼ばれているものとは異なる
言語内DSLなので自前でパーサやレキサーを作ることはない
計算結果の型を示すためのTagと呼ばれる機構を利用しないことからTagless
FinalとはInitialではないという意味(後述)
Initial Embedding
EnumでDSLの各項を表現しようとする手法
DSLを素朴にやろうとするとこれになる
code:scala
enum Calc:
case Plus(x: Int, y: Int)
case Mult(x: Int, y: Int)
後からこのCalcに対する解釈を行う部品を作ればDSLができるやん、という発想
Final Embedding
traitにメソッドを定義して各項を表現しようとする手法
code:scala
trait Calc:
type Repr = Int
def lit(x: Int): Repr
def plus(x: Repr, y: Repr): Repr
def mult(x: Repr, y: Repr): Repr
後からこのCalcを継承して解釈を行うobjectやclassを作ればDSLができるやん、という発想
この例だとIntを用いた実装しかできない
型パラメータを使う
ReprをCalcの型パラメータに押し出す
code:scala
trait CalcRepr:
def lit(x: Int): Repr
def plus(x: Repr, y: Repr): Repr
def mult(x: Repr, y: Repr): Repr
すると実装の幅が広がる
実際に計算をするclassを作ればそれはインタープリタになる
式を文字列として組み立てるようなclassを作ればそれはpretty printerになる
シンタックスはtraitで、セマンティクスは継承先で行う
実例
code:taglessfinal.scala
trait CalcRepr:
def lit(x: Int): Repr
def plus(x: Repr, y: Repr): Repr
def mult(x: Repr, y: Repr): Repr
object IntCalc extends CalcInt:
def lit(x: Int): Int = x
def plus(x: Int, y: Int): Int = x + y
def mult(x: Int, y: Int): Int = x * y
locally {
import IntCalc.*
plus(mult(lit(3), lit(2)), mult(lit(4), lit(3))) // => 18
}
object AstCalc extends CalcString:
def lit(x: Int): String = s"$x"
def plus(x: String, y: String): String = s"($x + $y)"
def mult(x: String, y: String): String = s"($x * $y)"
locally {
import AstCalc.*
plus(mult(lit(3), lit(2)), mult(lit(4), lit(3))) // => "((3 * 2) + (4 * 3))"
}
Final Embeddingは拡張に強い
Initial Embeddingだと、後からNeg(x: Int)といった構文要素が増えると関連箇所が全てコンパイルエラーになる
matchの網羅性が失われるから
Final Embeddingだと、特に問題にならない
code:scala
trait NegCalcExtensionRepr:
def neg(x: Repr): Repr
object NegCalc extends NegCalcExtensionInt:
def neg(x: Int): Int = -x
locally {
import IntCalc.*
import NegCalc.*
neg(plus(mult(lit(3), lit(2)), mult(lit(4), lit(3)))) // => 18
}
メリット
拡張しやすい
参考文献
Tagless Final & Scala 3 - Speaker Deck